/*
 * Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.api.client.generator.common.java;

import com.google.api.client.generator.common.AbstractFileGenerator;
import com.google.api.client.generator.linewrap.JavaLineWrapper;
import com.google.api.client.generator.linewrap.LineWrapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Map;
import java.util.SortedSet;
import java.util.regex.Pattern;

/**
 * @author Yaniv Inbar
 */
public abstract class AbstractJavaFileGenerator extends AbstractFileGenerator {

  /**
   * Class name pattern allowed in this class.
   *
   * <p/>
   * It is a subset of <a
   * href="http://java.sun.com/docs/books/jls/third_edition/html/lexical.html#40625"> identifiers
   * allowed by the Java language specification</a>. The only characters missing here but allowed in
   * the specification are {@code '$'} and {@code '_'}, neither of which are recommended to use
   * anyway. See also {@link Character#isJavaIdentifierStart(char)} and
   * {@link Character#isJavaIdentifierPart(char)}.
   */
  private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("[A-Z][A-Za-z0-9]*");

  /**
   * Package name pattern allowed in this class. It is a subset of <a
   * href="http://java.sun.com/docs/books/jls/third_edition/html/names.html#6.5"> package names
   * allowed by the Java language specification</a>. The restriction made here beyond the
   * specification is that only letters and digits are allowed in the package name, and that the use
   * of the default package is not allowed.
   */
  private static final Pattern PACKAGE_NAME_PATTERN =
      Pattern.compile("[a-z][a-z0-9]*(\\.[a-z][a-zA-Z0-9]*)*");

  /**
   * Map from the class name to its associated import's fully-qualified class name.
   */
  private final Map<String, String> fullyQualifiedImportFromClassName = Maps.newHashMap();

  /** Google imports. */
  private final SortedSet<String> googleImports = Sets.newTreeSet();

  /** Third-party imports. */
  private final SortedSet<String> otherImports = Sets.newTreeSet();

  /** Java SDK imports. */
  private final SortedSet<String> javaImports = Sets.newTreeSet();

  /** Java package name for generated class. */
  private final String packageName;

  /** Java class name for generated class. */
  private final String className;

  /** Java fully-qualified class name for generated class. */
  private final String fullyQualifiedClassName;

  /**
   * Output file name based on the class name, for example {@code "MyFile.java"}.
   */
  private final String outputFileName;

  /**
   * Output file path based on the fully-qualified class name relative to the source folder, for
   * example {@code "org/example/package/MyFile.java"}.
   */
  private final String outputRelativeFilePath;

  /** Root output file path. */
  private final String rootOutputFilePath;

  /**
   * @param rootOutputFilePath root output file path
   * @param packageName name of java package containing this source file
   * @param className class name for this java source file
   */
  protected AbstractJavaFileGenerator(
      String rootOutputFilePath, String packageName, String className) {
    this.rootOutputFilePath = rootOutputFilePath;
    Preconditions.checkArgument(PACKAGE_NAME_PATTERN.matcher(packageName).matches(), packageName);
    Preconditions.checkArgument(CLASS_NAME_PATTERN.matcher(className).matches(), className);
    this.packageName = packageName;
    this.className = className;
    fullyQualifiedClassName = packageName + '.' + className;
    // ensures that there is no clash between this class and other used classes
    addImport(fullyQualifiedClassName);
    outputFileName = className + ".java";
    outputRelativeFilePath = fullyQualifiedClassName.replace('.', '/') + ".java";
  }

  @Override
  public final LineWrapper getLineWrapper() {
    return JavaLineWrapper.get();
  }

  @Override
  public final String getOutputFilePath() {
    return rootOutputFilePath + packageName.replace('.', '/') + "/" + className + ".java";
  }

  @Override
  protected final void generate(PrintWriter out) {
    StringWriter stringWriter = new StringWriter();
    PrintWriter stringPrintWriter = new PrintWriter(stringWriter);
    generateBody(stringPrintWriter);
    String content = stringWriter.toString();
    generateHeader(out);
    writeImportDeclarations(out);
    out.println();
    out.print(content);
    out.close();
  }

  protected abstract void generateBody(PrintWriter out);

  private void generateHeader(PrintWriter out) {
    out.println("/*");
    out.println(" * Copyright (c) 2011 Google Inc.");
    out.println(" *");
    out.println(" * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not");
    out.println(" * use this file except in compliance with the License. You may obtain a copy of");
    out.println(" * the License at");
    out.println(" *");
    out.println(" * http://www.apache.org/licenses/LICENSE-2.0");
    out.println(" *");
    out.println(" * Unless required by applicable law or agreed to in writing, software");
    out.println(" * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT");
    out.println(" * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the");
    out.println(" * License for the specific language governing permissions and limitations under");
    out.println(" * the License.");
    out.println(" */");
    out.println();
    out.println("package " + packageName + ";");
    out.println();
  }


  public static String indent(int numSpaces) {
    return "                    ".substring(0, numSpaces);
  }

  /** Returns an unmodifiable view of the sorted set of Google imports. */
  public final SortedSet<String> getGoogleImports() {
    return Collections.unmodifiableSortedSet(googleImports);
  }

  /**
   * Returns an unmodifiable view of the sorted set of the third-party imports.
   */
  public final SortedSet<String> getOtherImports() {
    return Collections.unmodifiableSortedSet(otherImports);
  }

  /** Returns an unmodifiable view of the sorted set of the Java SDK imports. */
  public final SortedSet<String> getJavaImports() {
    return Collections.unmodifiableSortedSet(javaImports);
  }

  /** Returns the Java class name for generated class. */
  public final String getClassName() {
    return className;
  }

  /** Returns the Java fully-qualified class name for generated class. */
  public final String getFullyQualifiedClassName() {
    return fullyQualifiedClassName;
  }

  /** Returns the Java package name for generated class. */
  public final String getPackageName() {
    return packageName;
  }

  /**
   * Returns the output file path based on the fully-qualified class name relative to the source
   * folder, for example {@code "org/example/package/MyFile.java"}.
   */
  public final String getOutputRelativeFilePath() {
    return outputRelativeFilePath;
  }

  /**
   * Returns the output file name based on the class name, for example {@code "MyFile.java"}.
   */
  public final String getOutputFileName() {
    return outputFileName;
  }

  /**
   * Declares a use of the given Java class, including possibly adding an import for it.
   *
   * @param clazz Java class, for example {@code List.class}
   * @return either the class name if the import was successful or a fully-qualified class name if
   *         the import was unsuccessful (in the case where the class name is already mapped to
   *         another import)
   */
  public final String useClass(Class<?> clazz) {
    return useClass(clazz.getName());
  }

  /**
   * Declares a use of the given fully-qualified name of a Java class, including possibly adding an
   * import for it.
   *
   * @param fullyQualifiedName fully-qualified name of the class to use
   * @return either the class name if the import was successful or a fully-qualified class name if
   *         the import was unsuccessful (in the case where the class name is already mapped to
   *         another import)
   */
  public String useClass(String fullyQualifiedName) {
    String pkg = getPackageName(fullyQualifiedName);
    int pkgLength = pkg.length();
    int dot = fullyQualifiedName.indexOf('.', pkgLength + 1);
    String classToImport = dot == -1 ? fullyQualifiedName : fullyQualifiedName.substring(0, dot);
    if (!addImport(classToImport)) {
      return fullyQualifiedName;
    }
    return fullyQualifiedName.substring(pkgLength + 1);
  }

  /**
   * Writes the import declarations. Uses any possible import declarations from
   * {@link #readJavaFile} or {@link #addImport(String)}.
   */
  private final void writeImportDeclarations(PrintWriter writer) {
    writeImports(writer, googleImports);
    writeImports(writer, otherImports);
    writeImports(writer, javaImports);
  }

  protected final void writeJavaDoc(PrintWriter out, int indentNumSpaces, String comment) {
    if (comment != null) {
      String indent = AbstractJavaFileGenerator.indent(indentNumSpaces);
      out.println(indent + "/**");
      out.println(indent + " * " + Preconditions.checkNotNull(comment));
      out.println(indent + " */");
    }
  }

  /**
   * Adds the given fully-qualified Java import to add to the source file, for example {@code
   * "com.google.gdata.data.BaseEntry"}.
   *
   * @return whether the import failed because the class name is already mappe to a different
   *         fully-qualified name
   */
  public final boolean addImport(String fullyQualifiedName) {
    // parse out package and class name
    String pkg = getPackageName(fullyQualifiedName);
    int lastDot = fullyQualifiedName.lastIndexOf('.');
    String cls = fullyQualifiedName.substring(lastDot + 1);
    // check if class name is already mapped
    String curImport = fullyQualifiedImportFromClassName.get(cls);
    if (curImport != null) {
      return curImport.equals(fullyQualifiedName);
    }
    fullyQualifiedImportFromClassName.put(cls, fullyQualifiedName);
    // ignore if import from same package or "java.lang"
    if (!packageName.equals(pkg) && !"java.lang".equals(pkg)) {
      // Google package
      if (fullyQualifiedName.startsWith("com.google.")) {
        googleImports.add(fullyQualifiedName);
      } else if (fullyQualifiedName.startsWith("java.")
          || fullyQualifiedName.startsWith("javax.")) {
        // Java package
        javaImports.add(fullyQualifiedName);
      } else {
        // third-party package
        otherImports.add(fullyQualifiedName);
      }
    }
    return true;
  }

  /**
   * Returns the package name for the given Java class or package name, assuming that class names
   * always start with a capital letter and package names always start with a lowercase letter.
   */
  public static String getPackageName(String classOrPackageName) {
    int lastDot = classOrPackageName.length();
    if (lastDot == 0) {
      return "";
    }
    while (true) {
      int nextDot = classOrPackageName.lastIndexOf('.', lastDot - 1);
      // check for error case of 2 dots in a row or starts/ends in dot
      Preconditions.checkArgument(nextDot + 1 < lastDot);
      if (Character.isLowerCase(classOrPackageName.charAt(nextDot + 1))) {
        // check for case that input string is already a package
        if (lastDot == classOrPackageName.length()) {
          return classOrPackageName;
        }
        return classOrPackageName.substring(0, lastDot);
      }
      if (nextDot == -1) {
        return "";
      }
      lastDot = nextDot;
    }
  }

  /** Prints the given imports into the given writer. */
  private static void writeImports(PrintWriter writer, SortedSet<String> imports) {
    if (!imports.isEmpty()) {
      writer.println();
      for (String anImport : imports) {
        writer.println("import " + anImport + ";");
      }
    }
  }
}
